import pako from 'pako';
import utils from './utils';
import util from '@ohos.util';
import logger from './logger';
import current from './current';
import buffer from '@ohos.buffer';
import webSocket from '@ohos.net.webSocket';
import apiClient from '../http/core/apiClient';
import danmakuItem, { danmakuType } from '../model/danmaku/danmakuItem';

export default class wsClient {
  private handleMsg: Function;
  private roomId: number = 0;
  private token: string = '';
  private apiClient: apiClient;
  private chatPort: number = 2243;
  private danmakus: Array<any> = [];
  private heartBeatTimer: number = 0;
  private wsClient: webSocket.WebSocket;
  private chatHost: string = 'chat.bilibili.com';
  private readonly defaults: string[] = ["livecmt-2.bilibili.com", "livecmt-1.bilibili.com"];

  constructor(roomId: string, handleMsg: Function) {
    this.handleMsg = handleMsg;
    try {
      this.apiClient = new apiClient();
      this.roomId = parseInt(roomId);
      this.wsClient = webSocket.createWebSocket();
      let self = this;
      this.wsClient.on('open', async (err, value) => {
        if (err)
          logger.error(err);
        else if (await self.sendJoinChannel())
          self.startHeartbeat();
      });
      this.wsClient.on('message', (err, value) => {
        if (err)
          logger.error(err)
        else if (typeof value !== 'string') {
          self.handleData(value).then(() => {
            let item = this.danmakus.shift();
            if (!utils.isEmpty(item?.message))
              self.handleMsg(item);
          });
        }
      });
      this.wsClient.on('error', () => {

      });
    } catch (e) {
      logger.error(e)
    }
  }

  public connect() {
    try {
      this.apiClient.getResult(`https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=${this.roomId}`).then((res) => {
        if (!utils.isEmpty(res)) {
          let item = res.data.host_list[2];
          this.chatHost = item.host.toString();
          this.token = res.data.token.toString();
          this.chatPort = parseInt(item.wss_port.toString());
          if (utils.isEmpty(this.chatHost))
            this.chatHost = this.defaults[0];
        } else
          this.chatHost = this.defaults[0];
        this.wsClient.connect(`wss://${this.chatHost}:${this.chatPort}/sub`, {
          header: {
            'Origin': 'https://live.bilibili.com',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
          }
        }).catch((err) => logger.error(err));
      });
    } catch (err) {
      logger.error(err);
    }
  }

  private startHeartbeat() {
    let self = this;
    self.sendSocketData(2);
    this.heartBeatTimer = setInterval(function () {
      self.sendSocketData(2);
    }, 30000);
  }

  private sendJoinChannel(): Promise<boolean> {
    let playload = JSON.stringify({
      roomid: this.roomId,
      uid: current.instance.mid,
      protover: 2,
      token: this.token,
      platform: "web"
    });
    return this.sendSocketData(7, playload);
  }

  private sendSocketData(action: number, body?: string): Promise<boolean> {
    if (utils.isEmpty(body))
      body = '';
    return this.sendSocketDataAsync(0, 16, 2, action, 1, body);
  }

  private sendSocketDataAsync(packetlength: number, magic: number, ver: number, action: number, param: number, body: string): Promise<boolean> {
    try {
      const bytearr = buffer.from(body);
      if (packetlength === 0)
        packetlength = bytearr.length + 16;
      let sendBytes = buffer.alloc(16);
      sendBytes.writeUInt32BE(packetlength, 0);
      sendBytes.writeUInt16BE(magic, 4);
      sendBytes.writeUInt16BE(ver, 6);
      sendBytes.writeUInt32BE(action, 8);
      sendBytes.writeUInt32BE(param, 12);
      if (bytearr.length > 0)
        sendBytes = buffer.concat([sendBytes, bytearr]);
      return this.send(sendBytes.buffer);
    } catch (e) {
      logger.error(e);
    }
  }

  private send(data: ArrayBuffer): Promise<boolean> {
    return new Promise((resolve) => {
      try {
        this.wsClient.send(data)
          .then(() => resolve(true))
          .catch((err) => {
            logger.error(err);
            resolve(false);
          });
      } catch (e) {
        logger.error(e);
      }
    });
  }

  public dispose() {
    try {
      clearInterval(this.heartBeatTimer);
      this.wsClient.off('error');
      this.wsClient.off('close');
      this.wsClient.off('message');
      this.wsClient.close();
    } catch (e) {
      logger.error(e)
    }
  }

  private async handleData(data: ArrayBuffer) {
    try {
      const dv = new DataView(data);
      const packageLen = dv.getUint32(0);
      const headerLen = dv.getUint16(4);
      const protover = dv.getUint16(6);
      const operation = dv.getUint32(8);
      const sequence = dv.getUint32(12);
      data = data.slice(headerLen, packageLen);
      switch (protover) {
        case 0: {
          const str = this.uintToString(new Uint8Array(data));
          this.parseDanmuMessage(str);
          break;
        }
        case 1: {
          const dataV = new DataView(data);
          if (operation === 3) {
            let danmu = new danmakuItem();
            danmu.message = dataV.getUint32(0);
            danmu.type = danmakuType.online;
            this.danmakus.push(danmu);
          } else if (operation === 8) {
            const str = this.uintToString(new Uint8Array(data));
            let danmu = new danmakuItem();
            danmu.message = str;
            danmu.type = danmakuType.unknow;
            this.danmakus.push(danmu);
          }
          break;
        }
        case 2: {
          if (operation === 5) {
            let p: Uint8Array = pako.inflate(new Uint8Array(data).buffer);
            this.unzip(p.buffer);
          }
          break;
        }
      }
    } catch (e) {
      logger.error(e);
    }
  }

  private uintToString(uintArray) {
    try {
      const decoder = util.TextDecoder.create("utf-8");
      return decoder.decodeWithStream(uintArray);
    } catch (e) {
      logger.error(e);
      return '';
    }
  }

  private parseDanmuMessage(json) {
    try {
      json = JSON.parse(json);
      switch (json.cmd) {
        case 'DANMU_MSG': {
          let danmu = new danmakuItem();
          danmu.name = json.info[2][1];
          danmu.message = json.info[1];
          if (!utils.isEmpty(json.info[2][2]) && parseInt(json.info[2][2].toString()) == 1) {
            danmu.isAdmin = true;
          }
          if (!utils.isEmpty(json.info[3]) && (json.info[3] as Array<any>)?.length > 0) {
            danmu.medalLv = json.info[3][0].toString();
            danmu.medalName = json.info[3][1].toString();
            danmu.medalColor = json.info[3][4].toString();
            danmu.hasMedal = true;
          }
          if (!utils.isEmpty(json.info[4]) && (json.info[4] as Array<any>)?.length > 0) {
            danmu.ul = `UL${json.info[4][0]}`;
            danmu.ulColor = json.info[4][2].toString();
            danmu.hasUl = true;
          }
          danmu.type = danmakuType.message;
          this.danmakus.push(danmu);
          break;
        }
        case 'WELCOME': {
          let danmu = new danmakuItem();
          danmu.uid = json.data.uid;
          danmu.name = json.data.uname;
          danmu.svip = parseInt(json.data.vip.toString()) != 1;
          danmu.type = danmakuType.gift;
          this.danmakus.push(danmu);
          break;
        }
        case 'SEND_GIFT': {
          let danmu = new danmakuItem();
          danmu.num = json.data.num;
          danmu.uid = json.data.uid;
          danmu.name = json.data.uname;
          danmu.action = json.data.action;
          danmu.giftId = json.data.giftId;
          danmu.giftName = json.data.giftName;
          danmu.type = danmakuType.gift;
          this.danmakus.push(danmu);
          break;
        }
        case 'SYS_MSG': {
          let danmu = new danmakuItem();
          danmu.message = json.msg;
          danmu.type = danmakuType.sys;
          this.danmakus.push(danmu);
          break;
        }
      }
    } catch (e) {
      logger.error(e);
    }
  }

  private unzip(data: ArrayBuffer) {
    try {
      var offect = 0;
      var len = 0
      const maxLength = data.byteLength;
      while (offect < maxLength) {
        data = data.slice(len, maxLength);
        const dv = new DataView(data);
        const packageLen = dv.getUint32(0);
        const headerLen = dv.getUint16(4);
        const protover = dv.getUint16(6);
        const operation = dv.getUint32(8);
        const sequence = dv.getUint32(12);
        var datas = data.slice(headerLen, packageLen)
        switch (protover) {
          case 0: {
            const str = this.uintToString(new Uint8Array(datas));
            this.parseDanmuMessage(str);
            break;
          }
          case 1: {
            const dataV = new DataView(datas);
            if (operation === 3) {
              let danmu = new danmakuItem();
              danmu.message = dataV.getUint32(0);
              danmu.type = danmakuType.online;
              this.danmakus.push(danmu);
            } else if (operation === 8) {
              const str = this.uintToString(new Uint8Array(datas));
              let danmu = new danmakuItem();
              danmu.message = str;
              danmu.type = danmakuType.unknow;
              this.danmakus.push(danmu);
            }
            break;
          }
          case 2: {
            if (operation == 5) {
              try {
                let p = pako.inflate(new Uint8Array(data).buffer, {
                  to: 'string'
                });
                let danmu = new danmakuItem();
                danmu.message = p;
                danmu.type = danmakuType.unknow;
                this.danmakus.push(danmu);
              } catch (e) {
                logger.error(e);
              }
            }
            break;
          }
        }
        offect += packageLen;
        len = packageLen;
      }
    } catch (e) {
      logger.error(e);
    }
  }
}